#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2022 F4PGA Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
""" Convert a PCF file into a VPR io.place file. """
import argparse
import csv
import sys
import re
from collections import defaultdict

import f4pga.utils.vpr_io_place as vpr_io_place
from f4pga.utils.pcf import parse_simple_pcf

# =============================================================================

# Known IOB types and VPR cells that can be placed at their sites
IOB_TYPES = {
    "CLOCK": [
        "PB-CLOCK",
    ],
    "BIDIR": [
        "PB-BIDIR",
    ],
    "SDIOMUX": [
        "PB-SDIOMUX",
    ],
}

BLOCK_INSTANCE_RE = re.compile(r"^(?P<name>\S+)\[(?P<index>[0-9]+)\]$")

# =============================================================================


def main():
    parser = argparse.ArgumentParser(description="Convert a PCF file into a VPR io.place file.")
    parser.add_argument("--pcf", "-p", "-P", type=argparse.FileType("r"), required=True, help="PCF input file")
    parser.add_argument("--blif", "-b", type=argparse.FileType("r"), required=True, help="BLIF / eBLIF file")
    parser.add_argument("--map", "-m", "-M", type=argparse.FileType("r"), required=True, help="Pin map CSV file")
    parser.add_argument(
        "--output", "-o", "-O", type=argparse.FileType("w"), default=sys.stdout, help="The output io.place file"
    )
    parser.add_argument("--net", "-n", type=argparse.FileType("r"), required=True, help="top.net file")

    args = parser.parse_args()

    io_place = vpr_io_place.IoPlace()
    io_place.read_io_list_from_eblif(args.blif)
    io_place.load_block_names_from_net_file(args.net)

    # Map of pad names to VPR locations.
    pad_map = defaultdict(lambda: dict())
    pad_alias_map = defaultdict(lambda: dict())

    for pin_map_entry in csv.DictReader(args.map):
        if pin_map_entry["type"] not in IOB_TYPES:
            continue

        name = pin_map_entry["name"]
        alias = ""
        if "alias" in pin_map_entry:
            alias = pin_map_entry["alias"]

        for t in IOB_TYPES[pin_map_entry["type"]]:
            pad_map[name][t] = (
                int(pin_map_entry["x"]),
                int(pin_map_entry["y"]),
                int(pin_map_entry["z"]),
            )
            if "alias" in pin_map_entry:
                pad_alias_map[alias] = name

    used_pads = set()

    for pcf_constraint in parse_simple_pcf(args.pcf):
        # Skip non-io constraints
        if type(pcf_constraint).__name__ != "PcfIoConstraint":
            continue

        pad_name = pcf_constraint.pad
        if not io_place.is_net(pcf_constraint.net):
            print(
                'PCF constraint "{}" from line {} constraints net {} \
                        which is not in available netlist:\n{}'.format(
                    pcf_constraint.line_str, pcf_constraint.line_num, pcf_constraint.net, "\n".join(io_place.get_nets())
                ),
                file=sys.stderr,
            )
            sys.exit(1)

        if pad_name not in pad_map and pad_name not in pad_alias_map:
            print(
                'PCF constraint "{}" from line {} constraints pad {} \
                        which is not in available pad map:\n{}'.format(
                    pcf_constraint.line_str, pcf_constraint.line_num, pad_name, "\n".join(sorted(pad_map.keys()))
                ),
                file=sys.stderr,
            )
            sys.exit(1)

        # Alias is specified in pcf file so assign it to corresponding pad name
        if pad_name in pad_alias_map:
            pad_name = pad_alias_map[pad_name]

        # Catch the same pad used multiple times
        if pad_name in used_pads:
            print(
                'PCF constraint "{}" from line {} constraints pad {} \
                        which has already been constrained'.format(
                    pcf_constraint.line_str, pcf_constraint.line_num, pad_name
                ),
                file=sys.stderr,
            )
            sys.exit(1)
        used_pads.add(pad_name)

        # Get the top-level block instance, strip its index
        inst = io_place.get_top_level_block_instance_for_net(pcf_constraint.net)
        if inst is None:
            continue

        match = BLOCK_INSTANCE_RE.match(inst)
        assert match is not None, inst

        inst = match.group("name")

        # Pick correct loc for that pb_type
        locs = pad_map[pad_name]
        if inst not in locs:
            print(
                'PCF constraint "{}" from line {} constraints net {} of a block type {} \
                        to a location for block types:\n{}'.format(
                    pcf_constraint.line_str,
                    pcf_constraint.line_num,
                    pcf_constraint.net,
                    inst,
                    "\n".join(sorted(list(locs.keys()))),
                ),
                file=sys.stderr,
            )
            sys.exit(1)

        # Constraint the net (block)
        loc = locs[inst]
        io_place.constrain_net(net_name=pcf_constraint.net, loc=loc, comment=pcf_constraint.line_str)

    io_place.output_io_place(args.output)


# =============================================================================

if __name__ == "__main__":
    main()
